package com.acme.cli;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.HashSet;
import java.util.Set;

import com.acme.bankapp.domine.account.Account;
import com.acme.bankapp.domine.account.CheckingAccount;
import com.acme.bankapp.domine.account.SavingAccount;
import com.acme.bankapp.domine.bank.Bank;
import com.acme.bankapp.domine.client.Client;
import com.acme.bankapp.domine.client.Gender;
import com.acme.bankapp.exceptions.ClientExistsException;
import com.acme.bankapp.exceptions.NoEnoughFundsException;
import com.acme.bankapp.exceptions.OverDraftLimitExceededException;
import com.acme.bankapp.service.bank.BankDataLoader;
import com.acme.bankapp.service.bank.BankService;
import com.acme.bankapp.service.bank.impl.BankServiceImpl;

/**
 * The following scenarios are available:
 * 1) "BankServer -m" In-memory mode.
 * Creates a few entities programmatically, prints them.
 * 2) "BankServer -s" Forces to save bank to bank.ser file
 * (file name can not be specified for simplicity)
 * 3) "BankServer -l" Loads the bank from bank.ser file
 * 4) "BankServer -lf" Loads feed file from bank.data file,
 * tries to add data to the bank and then saves it to bank.ser file
 * 5) "BankServer -server" Server mode.
 */
public class BankServer {

    private ServerSocket providerSocket;
    private Socket connection = null;
    private ObjectOutputStream out;
    private ObjectInputStream in;
    private String message;
    private Bank bank;

    private static final String ALREADY_LOADED =
            "Bank has already been loaded via another program argument. "
            + "Only one option from the list {-m, -lf, -l} "
            + "may be specified at a time.";
    private static final String BANK_IS_NOT_LOAD_MESSAGE = "Bank is not loaded";
    private static final String WAITING_CONNECT_MESSAGE =
            "Waiting for connection";
    private static final String SUCCESS_CONNECT_MESSAGE =
            "Connection successful";
    private static final String INVALID_DATA_EXCEPTION_MESSAGE =
            "Data received in unknown format";
    private static final String CLIENT_ADDED_MESSAGE =
            "Client is added successfully";
    private static final String INVALID_COMMAND = "Invalid command";

    private static final String STOP_SERVER_COMMAND = "bye";
    private static final String ADD_CLIENT_COMMAND = "add";
    private static final String PRINT_BANK_COMMAND = "print";
    private static final String DELINMIER_COMMAND = " ";

    private static final String SER_FILE = "bank.ser";
    private static final String FEED_FILE = "bank.dat";

    private static final int PORT = 2004;
    private static final int BACKLOG = 10;

    private static BankDataLoader bankDataLoader;
    private static BankService bankService;

    BankServer(final Bank bank) {
        this.bank = bank;
    }

    public static void main(final String[] args) throws IOException {
        Set<String> arguments = new HashSet<String>();
        for (String string : args) {
            arguments.add(string);
        }
        initServices();

        boolean bankLoaded = false;
        Bank bank = null;
        if (arguments.contains("-m")) {
            bank = createInMemoryBank();
            bankLoaded = true;
        }

        if (arguments.contains("-lf")) {
            if (bankLoaded) {
                System.out.println(ALREADY_LOADED);
                return;
            }
            bank = loadBankFromFeed();
            bankLoaded = true;
        }

        if (arguments.contains("-l")) {
            if (bankLoaded) {
                System.out.println(ALREADY_LOADED);
                return;
            }
            bank = loadBankFromDataSource();
            bankLoaded = true;
        }
        if (bank == null) {
            System.out.println(BANK_IS_NOT_LOAD_MESSAGE);
            return;
        }

        if (arguments.contains("-server")) {
            startServer(bank);
        }

        bankService.printBalance(bank);
        bankService.printMaximumAmountToWithdraw(bank);

        if (arguments.contains("-s")) {
            saveTheBankToDataSource(bank);
        }

    }

    private static void initServices() {
        bankService = new BankServiceImpl();
        bankDataLoader = new BankDataLoader();
        bankDataLoader.setBankService(bankService);

    }

    private static void saveTheBankToDataSource(final Bank bank)
            throws IOException {
        bankService.saveBank(bank, SER_FILE);
    }

    private static Bank loadBankFromDataSource() throws IOException {
        return bankService.loadBank(SER_FILE);
    }

    private static Bank loadBankFromFeed() throws IOException {
        Bank bank = new Bank();
        bankDataLoader.load(bank, FEED_FILE);
        saveTheBankToDataSource(bank);
        return bank;
    }

    private static Bank createInMemoryBank() {

        Bank bank = bankService.createNewBank();

        Client client1 = null;
        Client client2 = null;

        try {
            Account account1 = new SavingAccount(100);
            Account account2 = new CheckingAccount(100, 10);
            Account account3 = new SavingAccount(50);
            Account account4 = new CheckingAccount(75, 20);

            try {
                client1 = bankService.addClient(bank, "John", Gender.MALE);
                client2 = bankService.addClient(bank, "Miranda", Gender.FEMALE);
                client1.addAccount(account1);
                client1.addAccount(account2);
                client2.addAccount(account3);
                client2.addAccount(account4);
            } catch (ClientExistsException e) {
                System.out.println(e.getMessage());
            }

            account1.deposit(100);
            try {
                account3.withdraw(50);
                account4.withdraw(80);
            } catch (OverDraftLimitExceededException e) {
                System.out.println(e.getMessage());
            } catch (NoEnoughFundsException e) {
                System.out.println(e.getMessage());
            }

        } catch (IllegalArgumentException e) {
            System.out.println(e.getMessage());
        }

        return bank;
    }

    void run() {
        try {
            providerSocket = new ServerSocket(PORT, BACKLOG);
            System.out.println(WAITING_CONNECT_MESSAGE);
            connection = providerSocket.accept();
            System.out.println("Connection received from "
                    + connection.getInetAddress().getHostName());
            out = new ObjectOutputStream(connection.getOutputStream());
            out.flush();
            in = new ObjectInputStream(connection.getInputStream());
            sendMessage(SUCCESS_CONNECT_MESSAGE);
            do {
                try {
                    message = (String) in.readObject();
                } catch (ClassNotFoundException e) {
                    System.err.println(INVALID_DATA_EXCEPTION_MESSAGE);
                }
                message = parseMessage(message);
                sendMessage(message);
            } while (!message.equals(STOP_SERVER_COMMAND));
        } catch (IOException e) {
            System.out.println(e.getMessage());
        } finally {
            try {
                in.close();
                out.close();
                providerSocket.close();
            } catch (IOException e) {
                System.out.println(e.getMessage());
            }
        }
    }

    private String parseMessage(final String message) {
        System.out.println("client> " + message);
        String[] messageParts = message.split(DELINMIER_COMMAND);
        String command = messageParts[0];
        switch (command) {
        case ADD_CLIENT_COMMAND:
            if (messageParts.length > 1) {
                BankDataLoader.addClientFromString(bank, messageParts[1]);
                return CLIENT_ADDED_MESSAGE;
            } else {
                return INVALID_COMMAND;
            }
        case PRINT_BANK_COMMAND:
            return bank.toString();
        case STOP_SERVER_COMMAND:
            return STOP_SERVER_COMMAND;
        default:
            return INVALID_COMMAND;
        }
    }

    void sendMessage(final String msg) {
        try {
            out.writeObject(msg);
            out.flush();
            System.out.println("server> " + msg);
        } catch (IOException e) {
            System.out.println(e.getMessage());
        }
    }

    public static void startServer(final Bank bank) {
        BankServer server = new BankServer(bank);
        while (true) {
            server.run();
        }
    }
}
